عملیات قدرتمند فایل در Node.js را با تایپاسکریپت فرا بگیرید. این راهنمای جامع به بررسی متدهای همگام، ناهمگام و استریم FS با تأکید بر ایمنی نوع، مدیریت خطا و بهترین شیوهها برای تیمهای توسعه جهانی میپردازد.
تسلط بر سیستم فایل در تایپاسکریپت: عملیات فایل در Node.js با ایمنی نوع برای توسعهدهندگان جهانی
در چشمانداز گسترده توسعه نرمافزار مدرن، Node.js به عنوان یک محیط اجرای قدرتمند برای ساخت برنامههای سمت سرور مقیاسپذیر، ابزارهای خط فرمان و موارد دیگر شناخته میشود. یک جنبه اساسی در بسیاری از برنامههای Node.js، تعامل با سیستم فایل است – خواندن، نوشتن، ایجاد و مدیریت فایلها و دایرکتوریها. در حالی که جاوااسکریپت انعطافپذیری لازم برای انجام این عملیات را فراهم میکند، معرفی تایپاسکریپت با ارائه بررسی نوع ایستا (static type-checking)، ابزارهای پیشرفته و در نهایت، قابلیت اطمینان و نگهداری بیشتر، این تجربه را ارتقا میبخشد.
این راهنمای جامع برای مخاطبان جهانی از توسعهدهندگان، صرفنظر از پیشینه فرهنگی یا موقعیت جغرافیاییشان، تهیه شده است که به دنبال تسلط بر عملیات فایل در Node.js با استحکامی هستند که تایپاسکریپت ارائه میدهد. ما به بررسی ماژول اصلی `fs`، کاوش در پارادایمهای مختلف همگام و ناهمگام آن، بررسی APIهای مدرن مبتنی بر Promise و کشف اینکه چگونه سیستم نوع تایپاسکریپت میتواند به طور قابل توجهی خطاهای رایج را کاهش داده و وضوح کد شما را بهبود بخشد، خواهیم پرداخت.
سنگ بنا: درک سیستم فایل Node.js (`fs`)
ماژول `fs` در Node.js یک API برای تعامل با سیستم فایل فراهم میکند که بر اساس توابع استاندارد POSIX مدلسازی شده است. این ماژول طیف گستردهای از متدها را ارائه میدهد، از خواندن و نوشتن ساده فایل گرفته تا دستکاریهای پیچیده دایرکتوری و نظارت بر فایلها. به طور سنتی، این عملیات با استفاده از callbackها انجام میشد که در سناریوهای پیچیده به «جهنم callback» (callback hell) بدنام منجر میشد. با تکامل Node.js، Promiseها و `async/await` به عنوان الگوهای ترجیحی برای عملیات ناهمگام ظهور کردهاند که کد را خواناتر و قابل مدیریتتر میکنند.
چرا تایپاسکریپت برای عملیات سیستم فایل؟
در حالی که ماژول `fs` در Node.js با جاوااسکریپت خالص به خوبی کار میکند، ادغام تایپاسکریپت چندین مزیت قانعکننده به همراه دارد:
- ایمنی نوع (Type Safety): خطاهای رایج مانند انواع آرگومانهای نادرست، پارامترهای جا افتاده یا مقادیر بازگشتی غیرمنتظره را در زمان کامپایل، حتی قبل از اجرای کد، شناسایی میکند. این ویژگی به ویژه هنگام کار با انکودینگهای مختلف فایل، فلگها و اشیاء `Buffer` بسیار ارزشمند است.
- خوانایی بهبود یافته: حاشیهنویسیهای صریح نوع (type annotations) مشخص میکنند که یک تابع چه نوع دادهای را انتظار دارد و چه چیزی را برمیگرداند، که درک کد را برای توسعهدهندگان در تیمهای مختلف بهبود میبخشد.
- ابزارها و تکمیل خودکار بهتر: IDEها (مانند VS Code) از تعاریف نوع تایپاسکریپت برای ارائه تکمیل خودکار هوشمند، راهنمایی پارامترها و مستندات درونخطی استفاده میکنند که به طور قابل توجهی بهرهوری را افزایش میدهد.
- اطمینان در بازسازی کد (Refactoring): هنگامی که شما یک رابط (interface) یا امضای یک تابع را تغییر میدهید، تایپاسکریپت بلافاصله تمام بخشهای تحت تأثیر را مشخص میکند و بازسازی کد در مقیاس بزرگ را کمتر مستعد خطا میکند.
- سازگاری جهانی: یک سبک کدنویسی و درک سازگار از ساختارهای داده را در بین تیمهای توسعه بینالمللی تضمین میکند و ابهام را کاهش میدهد.
عملیات همگام در مقابل ناهمگام: یک دیدگاه جهانی
درک تمایز بین عملیات همگام و ناهمگام بسیار مهم است، به ویژه هنگام ساخت برنامههایی برای استقرار جهانی که در آن عملکرد و پاسخگویی از اهمیت بالایی برخوردار است. اکثر توابع ماژول `fs` در دو نسخه همگام و ناهمگام ارائه میشوند. به عنوان یک قاعده کلی، متدهای ناهمگام برای عملیات ورودی/خروجی غیرمسدودکننده (non-blocking I/O) ترجیح داده میشوند که برای حفظ پاسخگویی سرور Node.js شما ضروری هستند.
- ناهمگام (غیرمسدودکننده - Non-blocking): این متدها یک تابع callback را به عنوان آخرین آرگومان خود میپذیرند یا یک `Promise` برمیگردانند. آنها عملیات سیستم فایل را آغاز کرده و بلافاصله برمیگردند و به کدهای دیگر اجازه اجرا میدهند. هنگامی که عملیات کامل میشود، callback فراخوانی میشود (یا Promise حل/رد میشود). این برای برنامههای سروری که درخواستهای همزمان متعددی از کاربران در سراسر جهان را مدیریت میکنند ایدهآل است، زیرا از مسدود شدن سرور در حین انتظار برای پایان عملیات فایل جلوگیری میکند.
- همگام (مسدودکننده - Blocking): این متدها قبل از بازگشت، عملیات را به طور کامل انجام میدهند. در حالی که کدنویسی آنها سادهتر است، آنها حلقه رویداد (event loop) Node.js را مسدود میکنند و از اجرای هر کد دیگری تا زمان اتمام عملیات سیستم فایل جلوگیری میکنند. این میتواند منجر به گلوگاههای عملکردی قابل توجه و برنامههای غیرپاسخگو شود، به ویژه در محیطهای با ترافیک بالا. از آنها به ندرت استفاده کنید، معمولاً برای منطق راهاندازی برنامه یا اسکریپتهای ساده که مسدود شدن قابل قبول است.
انواع اصلی عملیات فایل در تایپاسکریپت
بیایید به کاربرد عملی تایپاسکریپت با عملیات رایج سیستم فایل بپردازیم. ما از تعاریف نوع داخلی برای Node.js استفاده خواهیم کرد که معمولاً از طریق بسته `@types/node` در دسترس هستند.
برای شروع، اطمینان حاصل کنید که تایپاسکریپت و تایپهای Node.js را در پروژه خود نصب کردهاید:
npm install typescript @types/node --save-dev
فایل `tsconfig.json` شما باید به درستی پیکربندی شده باشد، به عنوان مثال:
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}
خواندن فایلها: `readFile`، `readFileSync` و Promises API
خواندن محتوا از فایلها یک عملیات اساسی است. تایپاسکریپت به شما کمک میکند تا مسیرهای فایل، انکودینگها و خطاهای احتمالی را به درستی مدیریت کنید.
خواندن ناهمگام فایل (مبتنی بر Callback)
تابع `fs.readFile` ابزار اصلی برای خواندن ناهمگام فایل است. این تابع مسیر، یک انکودینگ اختیاری و یک تابع callback را میپذیرد. تایپاسکریپت تضمین میکند که آرگومانهای callback به درستی تایپگذاری شدهاند (`Error | null`، `Buffer | string`).
import * as fs from 'fs';
const filePath: string = 'data/example.txt';
fs.readFile(filePath, 'utf8', (err: NodeJS.ErrnoException | null, data: string) => {
if (err) {
// ثبت خطا برای دیباگ بینالمللی، مثلاً 'فایل یافت نشد'
console.error(`Error reading file '${filePath}': ${err.message}`);
return;
}
// پردازش محتوای فایل، با اطمینان از اینکه طبق انکودینگ 'utf8' یک رشته است
console.log(`File content (${filePath}):\n${data}`);
});
// مثال: خواندن دادههای باینری (انکودینگ مشخص نشده)
const binaryFilePath: string = 'data/image.png';
fs.readFile(binaryFilePath, (err: NodeJS.ErrnoException | null, data: Buffer) => {
if (err) {
console.error(`Error reading binary file '${binaryFilePath}': ${err.message}`);
return;
}
// در اینجا 'data' یک بافر است، آماده برای پردازش بیشتر (مثلاً استریم به یک کلاینت)
console.log(`Read ${data.byteLength} bytes from ${binaryFilePath}`);
});
خواندن همگام فایل
`fs.readFileSync` حلقه رویداد را مسدود میکند. نوع بازگشتی آن بسته به اینکه انکودینگ ارائه شده باشد یا نه، `Buffer` یا `string` است. تایپاسکریپت این را به درستی استنباط میکند.
import * as fs from 'fs';
const syncFilePath: string = 'data/sync_example.txt';
try {
const content: string = fs.readFileSync(syncFilePath, 'utf8');
console.log(`Synchronous read content (${syncFilePath}):\n${content}`);
} catch (error: any) {
console.error(`Synchronous read error for '${syncFilePath}': ${error.message}`);
}
خواندن فایل مبتنی بر Promise (`fs/promises`)
API مدرن `fs/promises` یک رابط تمیزتر و مبتنی بر Promise ارائه میدهد که برای عملیات ناهمگام بسیار توصیه میشود. تایپاسکریپت در اینجا، به ویژه با `async/await`، عالی عمل میکند.
import * as fsPromises from 'fs/promises';
async function readTextFile(path: string): Promise
نوشتن فایلها: `writeFile`، `writeFileSync` و فلگها
نوشتن داده در فایلها به همان اندازه حیاتی است. تایپاسکریپت به مدیریت مسیرهای فایل، انواع داده (رشته یا بافر)، انکودینگ و فلگهای باز کردن فایل کمک میکند.
نوشتن ناهمگام فایل
`fs.writeFile` برای نوشتن داده در یک فایل استفاده میشود و به طور پیشفرض در صورت وجود فایل، آن را جایگزین میکند. شما میتوانید این رفتار را با `flags` کنترل کنید.
import * as fs from 'fs';
const outputFilePath: string = 'data/output.txt';
const fileContent: string = 'This is new content written by TypeScript.';
fs.writeFile(outputFilePath, fileContent, 'utf8', (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`Error writing file '${outputFilePath}': ${err.message}`);
return;
}
console.log(`File '${outputFilePath}' written successfully.`);
});
// مثال با داده بافر
const bufferContent: Buffer = Buffer.from('Binary data example');
const binaryOutputFilePath: string = 'data/binary_output.bin';
fs.writeFile(binaryOutputFilePath, bufferContent, (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`Error writing binary file '${binaryOutputFilePath}': ${err.message}`);
return;
}
console.log(`Binary file '${binaryOutputFilePath}' written successfully.`);
});
نوشتن همگام فایل
`fs.writeFileSync` حلقه رویداد را تا زمان تکمیل عملیات نوشتن مسدود میکند.
import * as fs from 'fs';
const syncOutputFilePath: string = 'data/sync_output.txt';
try {
fs.writeFileSync(syncOutputFilePath, 'Synchronously written content.', 'utf8');
console.log(`File '${syncOutputFilePath}' written synchronously.`);
} catch (error: any) {
console.error(`Synchronous write error for '${syncOutputFilePath}': ${error.message}`);
}
نوشتن فایل مبتنی بر Promise (`fs/promises`)
رویکرد مدرن با `async/await` و `fs/promises` اغلب برای مدیریت نوشتنهای ناهمگام تمیزتر است.
import * as fsPromises from 'fs/promises';
import { constants as fsConstants } from 'fs'; // برای فلگها
async function writeDataToFile(path: string, data: string | Buffer): Promise
فلگهای مهم:
- `'w'` (پیشفرض): باز کردن فایل برای نوشتن. فایل ایجاد میشود (اگر وجود نداشته باشد) یا کوتاه میشود (اگر وجود داشته باشد).
- `'w+'`: باز کردن فایل برای خواندن و نوشتن. فایل ایجاد میشود (اگر وجود نداشته باشد) یا کوتاه میشود (اگر وجود داشته باشد).
- `'a'` (الحاق): باز کردن فایل برای الحاق. فایل در صورت عدم وجود ایجاد میشود.
- `'a+'`: باز کردن فایل برای خواندن و الحاق. فایل در صورت عدم وجود ایجاد میشود.
- `'r'` (خواندن): باز کردن فایل برای خواندن. اگر فایل وجود نداشته باشد، یک استثنا رخ میدهد.
- `'r+'`: باز کردن فایل برای خواندن و نوشتن. اگر فایل وجود نداشته باشد، یک استثنا رخ میدهد.
- `'wx'` (نوشتن انحصاری): مانند `'w'` اما اگر مسیر وجود داشته باشد، با شکست مواجه میشود.
- `'ax'` (الحاق انحصاری): مانند `'a'` اما اگر مسیر وجود داشته باشد، با شکست مواجه میشود.
الحاق به فایلها: `appendFile`، `appendFileSync`
هنگامی که نیاز به افزودن داده به انتهای یک فایل موجود بدون بازنویسی محتوای آن دارید، `appendFile` انتخاب شماست. این به ویژه برای ثبت وقایع (logging)، جمعآوری دادهها یا ردپاهای حسابرسی (audit trails) مفید است.
الحاق ناهمگام
import * as fs from 'fs';
const logFilePath: string = 'data/app_logs.log';
function logMessage(message: string): void {
const timestamp: string = new Date().toISOString();
const logEntry: string = `${timestamp} - ${message}\n`;
fs.appendFile(logFilePath, logEntry, 'utf8', (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`Error appending to log file '${logFilePath}': ${err.message}`);
return;
}
console.log(`Logged message to '${logFilePath}'.`);
});
}
logMessage('User "Alice" logged in.');
setTimeout(() => logMessage('System update initiated.'), 50);
logMessage('Database connection established.');
الحاق همگام
import * as fs from 'fs';
const syncLogFilePath: string = 'data/sync_app_logs.log';
function logMessageSync(message: string): void {
const timestamp: string = new Date().toISOString();
const logEntry: string = `${timestamp} - ${message}\n`;
try {
fs.appendFileSync(syncLogFilePath, logEntry, 'utf8');
console.log(`Logged message synchronously to '${syncLogFilePath}'.`);
} catch (error: any) {
console.error(`Synchronous error appending to log file '${syncLogFilePath}': ${error.message}`);
}
}
logMessageSync('Application started.');
logMessageSync('Configuration loaded.');
الحاق مبتنی بر Promise (`fs/promises`)
import * as fsPromises from 'fs/promises';
const promiseLogFilePath: string = 'data/promise_app_logs.log';
async function logMessagePromise(message: string): Promise
حذف فایلها: `unlink`، `unlinkSync`
حذف فایلها از سیستم فایل. تایپاسکریپت به شما کمک میکند تا اطمینان حاصل کنید که یک مسیر معتبر را ارسال کرده و خطاها را به درستی مدیریت میکنید.
حذف ناهمگام
import * as fs from 'fs';
const fileToDeletePath: string = 'data/temp_to_delete.txt';
// ابتدا فایل را ایجاد میکنیم تا برای نمایش حذف وجود داشته باشد
fs.writeFile(fileToDeletePath, 'Temporary content.', 'utf8', (err) => {
if (err) {
console.error('Error creating file for deletion demo:', err);
return;
}
console.log(`File '${fileToDeletePath}' created for deletion demo.`);
fs.unlink(fileToDeletePath, (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`Error deleting file '${fileToDeletePath}': ${err.message}`);
return;
}
console.log(`File '${fileToDeletePath}' deleted successfully.`);
});
});
حذف همگام
import * as fs from 'fs';
const syncFileToDeletePath: string = 'data/sync_temp_to_delete.txt';
try {
fs.writeFileSync(syncFileToDeletePath, 'Sync temp content.', 'utf8');
console.log(`File '${syncFileToDeletePath}' created.`);
fs.unlinkSync(syncFileToDeletePath);
console.log(`File '${syncFileToDeletePath}' deleted synchronously.`);
} catch (error: any) {
console.error(`Synchronous deletion error for '${syncFileToDeletePath}': ${error.message}`);
}
حذف مبتنی بر Promise (`fs/promises`)
import * as fsPromises from 'fs/promises';
const promiseFileToDeletePath: string = 'data/promise_temp_to_delete.txt';
async function deleteFile(path: string): Promise
بررسی وجود و مجوزهای فایل: `existsSync`، `access`، `accessSync`
قبل از انجام عملیات روی یک فایل، ممکن است نیاز داشته باشید بررسی کنید که آیا آن وجود دارد یا آیا فرآیند فعلی مجوزهای لازم را دارد. تایپاسکریپت با ارائه تایپ برای پارامتر `mode` کمک میکند.
بررسی وجود همگام
`fs.existsSync` یک بررسی ساده و همگام است. در حالی که راحت است، آسیبپذیری شرایط رقابتی (race condition) را دارد (ممکن است یک فایل بین `existsSync` و یک عملیات بعدی حذف شود)، بنابراین اغلب بهتر است برای عملیات حیاتی از `fs.access` استفاده کنید.
import * as fs from 'fs';
const checkFilePath: string = 'data/example.txt';
if (fs.existsSync(checkFilePath)) {
console.log(`File '${checkFilePath}' exists.`);
} else {
console.log(`File '${checkFilePath}' does not exist.`);
}
بررسی ناهمگام مجوز (`fs.access`)
`fs.access` مجوزهای کاربر را برای فایل یا دایرکتوری مشخص شده توسط `path` آزمایش میکند. این ناهمگام است و یک آرگومان `mode` میپذیرد (به عنوان مثال، `fs.constants.F_OK` برای وجود، `R_OK` برای خواندن، `W_OK` برای نوشتن، `X_OK` برای اجرا).
import * as fs from 'fs';
import { constants } from 'fs';
const accessFilePath: string = 'data/example.txt';
fs.access(accessFilePath, constants.F_OK, (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`File '${accessFilePath}' does not exist or access denied.`);
return;
}
console.log(`File '${accessFilePath}' exists.`);
});
fs.access(accessFilePath, constants.R_OK | constants.W_OK, (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`File '${accessFilePath}' is not readable/writable or access denied: ${err.message}`);
return;
}
console.log(`File '${accessFilePath}' is readable and writable.`);
});
بررسی مجوز مبتنی بر Promise (`fs/promises`)
import * as fsPromises from 'fs/promises';
import { constants } from 'fs';
async function checkFilePermissions(path: string, mode: number): Promise
دریافت اطلاعات فایل: `stat`، `statSync`، `fs.Stats`
خانواده توابع `fs.stat` اطلاعات دقیقی در مورد یک فایل یا دایرکتوری ارائه میدهد، مانند اندازه، تاریخ ایجاد، تاریخ اصلاح و مجوزها. رابط `fs.Stats` تایپاسکریپت کار با این دادهها را بسیار ساختاریافته و قابل اعتماد میکند.
Stat ناهمگام
import * as fs from 'fs';
import { Stats } from 'fs';
const statFilePath: string = 'data/example.txt';
fs.stat(statFilePath, (err: NodeJS.ErrnoException | null, stats: Stats) => {
if (err) {
console.error(`Error getting stats for '${statFilePath}': ${err.message}`);
return;
}
console.log(`Stats for '${statFilePath}':`);
console.log(` Is file: ${stats.isFile()}`);
console.log(` Is directory: ${stats.isDirectory()}`);
console.log(` Size: ${stats.size} bytes`);
console.log(` Creation time: ${stats.birthtime.toISOString()}`);
console.log(` Last modified: ${stats.mtime.toISOString()}`);
});
Stat مبتنی بر Promise (`fs/promises`)
import * as fsPromises from 'fs/promises';
import { Stats } from 'fs'; // هنوز از رابط Stats ماژول 'fs' استفاده میشود
async function getFileStats(path: string): Promise
عملیات دایرکتوری با تایپاسکریپت
مدیریت دایرکتوریها یک نیاز رایج برای سازماندهی فایلها، ایجاد فضای ذخیرهسازی خاص برنامه یا مدیریت دادههای موقت است. تایپاسکریپت تایپگذاری قوی برای این عملیات فراهم میکند.
ایجاد دایرکتوریها: `mkdir`، `mkdirSync`
تابع `fs.mkdir` برای ایجاد دایرکتوریهای جدید استفاده میشود. گزینه `recursive` برای ایجاد دایرکتوریهای والد در صورت عدم وجود آنها بسیار مفید است و رفتاری شبیه `mkdir -p` در سیستمهای شبه یونیکس را تقلید میکند.
ایجاد ناهمگام دایرکتوری
import * as fs from 'fs';
const newDirPath: string = 'data/new_directory';
const recursiveDirPath: string = 'data/nested/path/to/create';
// ایجاد یک دایرکتوری تکی
fs.mkdir(newDirPath, (err: NodeJS.ErrnoException | null) => {
if (err) {
// خطای EEXIST را اگر دایرکتوری از قبل وجود دارد، نادیده بگیر
if (err.code === 'EEXIST') {
console.log(`Directory '${newDirPath}' already exists.`);
} else {
console.error(`Error creating directory '${newDirPath}': ${err.message}`);
}
return;
}
console.log(`Directory '${newDirPath}' created successfully.`);
});
// ایجاد دایرکتوریهای تودرتو به صورت بازگشتی
fs.mkdir(recursiveDirPath, { recursive: true }, (err: NodeJS.ErrnoException | null) => {
if (err) {
if (err.code === 'EEXIST') {
console.log(`Directory '${recursiveDirPath}' already exists.`);
} else {
console.error(`Error creating recursive directory '${recursiveDirPath}': ${err.message}`);
}
return;
}
console.log(`Recursive directories '${recursiveDirPath}' created successfully.`);
});
ایجاد دایرکتوری مبتنی بر Promise (`fs/promises`)
import * as fsPromises from 'fs/promises';
async function createDirectory(path: string, recursive: boolean = false): Promise
خواندن محتویات دایرکتوری: `readdir`، `readdirSync`، `fs.Dirent`
برای لیست کردن فایلها و زیردایرکتوریهای داخل یک دایرکتوری مشخص، از `fs.readdir` استفاده میکنید. گزینه `withFileTypes` یک افزودنی مدرن است که اشیاء `fs.Dirent` را برمیگرداند و اطلاعات دقیقتری را مستقیماً بدون نیاز به `stat` کردن هر ورودی به صورت جداگانه فراهم میکند.
خواندن ناهمگام دایرکتوری
import * as fs from 'fs';
const readDirPath: string = 'data';
fs.readdir(readDirPath, (err: NodeJS.ErrnoException | null, files: string[]) => {
if (err) {
console.error(`Error reading directory '${readDirPath}': ${err.message}`);
return;
}
console.log(`Contents of directory '${readDirPath}':`);
files.forEach(file => {
console.log(` - ${file}`);
});
});
// با گزینه `withFileTypes`
fs.readdir(readDirPath, { withFileTypes: true }, (err: NodeJS.ErrnoException | null, dirents: fs.Dirent[]) => {
if (err) {
console.error(`Error reading directory with file types '${readDirPath}': ${err.message}`);
return;
}
console.log(`Contents of directory '${readDirPath}' (with types):`);
dirents.forEach(dirent => {
const type: string = dirent.isFile() ? 'File' : dirent.isDirectory() ? 'Directory' : 'Other';
console.log(` - ${dirent.name} (${type})`);
});
});
خواندن دایرکتوری مبتنی بر Promise (`fs/promises`)
import * as fsPromises from 'fs/promises';
import { Dirent } from 'fs'; // هنوز از رابط Dirent ماژول 'fs' استفاده میشود
async function listDirectoryContents(path: string): Promise
حذف دایرکتوریها: `rmdir` (منسوخ)، `rm`، `rmSync`
Node.js متدهای حذف دایرکتوری خود را تکامل داده است. `fs.rmdir` اکنون عمدتاً توسط `fs.rm` برای حذفهای بازگشتی جایگزین شده است و یک API قویتر و سازگارتر ارائه میدهد.
حذف ناهمگام دایرکتوری (`fs.rm`)
تابع `fs.rm` (موجود از Node.js 14.14.0) روش توصیه شده برای حذف فایلها و دایرکتوریها است. گزینه `recursive: true` برای حذف دایرکتوریهای غیرخالی حیاتی است.
import * as fs from 'fs';
const dirToDeletePath: string = 'data/dir_to_delete';
const nestedDirToDeletePath: string = 'data/nested_dir/sub';
// آمادهسازی: ایجاد یک دایرکتوری با یک فایل داخل آن برای نمایش حذف بازگشتی
fs.mkdir(nestedDirToDeletePath, { recursive: true }, (err) => {
if (err && err.code !== 'EEXIST') {
console.error('Error creating nested directory for demo:', err);
return;
}
fs.writeFile(`${nestedDirToDeletePath}/file_inside.txt`, 'Some content', (err) => {
if (err) { console.error('Error creating file inside nested directory:', err); return; }
console.log(`Directory '${nestedDirToDeletePath}' and file created for deletion demo.`);
fs.rm(nestedDirToDeletePath, { recursive: true, force: true }, (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`Error deleting recursive directory '${nestedDirToDeletePath}': ${err.message}`);
return;
}
console.log(`Recursive directory '${nestedDirToDeletePath}' deleted successfully.`);
});
});
});
// حذف یک دایرکتوری خالی
fs.mkdir(dirToDeletePath, (err) => {
if (err && err.code !== 'EEXIST') {
console.error('Error creating empty directory for demo:', err);
return;
}
console.log(`Directory '${dirToDeletePath}' created for deletion demo.`);
fs.rm(dirToDeletePath, { recursive: false }, (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`Error deleting empty directory '${dirToDeletePath}': ${err.message}`);
return;
}
console.log(`Empty directory '${dirToDeletePath}' deleted successfully.`);
});
});
حذف دایرکتوری مبتنی بر Promise (`fs/promises`)
import * as fsPromises from 'fs/promises';
async function deleteDirectory(path: string, recursive: boolean = false): Promise
مفاهیم پیشرفته سیستم فایل با تایپاسکریپت
فراتر از عملیات اولیه خواندن/نوشتن، Node.js ویژگیهای قدرتمندی برای مدیریت فایلهای بزرگتر، جریانهای داده پیوسته و نظارت بیدرنگ بر سیستم فایل ارائه میدهد. تعاریف نوع تایپاسکریپت به زیبایی به این سناریوهای پیشرفته گسترش مییابند و استحکام را تضمین میکنند.
توصیفگرهای فایل و استریمها
برای فایلهای بسیار بزرگ یا زمانی که به کنترل دقیق بر دسترسی به فایل نیاز دارید (مثلاً موقعیتهای خاص در یک فایل)، توصیفگرهای فایل (file descriptors) و استریمها (streams) ضروری میشوند. استریمها راهی کارآمد برای مدیریت خواندن یا نوشتن مقادیر زیاد داده به صورت تکهای (chunks) فراهم میکنند، به جای بارگذاری کل فایل در حافظه، که برای برنامههای مقیاسپذیر و مدیریت کارآمد منابع در سرورها در سطح جهانی حیاتی است.
باز و بسته کردن فایلها با توصیفگرها (`fs.open`, `fs.close`)
توصیفگر فایل یک شناسه منحصر به فرد (یک عدد) است که توسط سیستم عامل به یک فایل باز اختصاص داده میشود. شما میتوانید از `fs.open` برای دریافت یک توصیفگر فایل استفاده کنید، سپس عملیاتی مانند `fs.read` یا `fs.write` را با استفاده از آن توصیفگر انجام دهید و در نهایت آن را با `fs.close` ببندید.
import * as fs from 'fs';
import { promises as fsPromises } from 'fs';
import { constants } from 'fs';
const descriptorFilePath: string = 'data/descriptor_example.txt';
async function demonstrateFileDescriptorOperations(): Promise
استریمهای فایل (`fs.createReadStream`, `fs.createWriteStream`)
استریمها برای مدیریت کارآمد فایلهای بزرگ قدرتمند هستند. `fs.createReadStream` و `fs.createWriteStream` به ترتیب استریمهای `Readable` و `Writable` را برمیگردانند که به طور یکپارچه با API استریمینگ Node.js ادغام میشوند. تایپاسکریپت تعاریف نوع عالی برای این رویدادهای استریم (مانند `'data'`، `'end'`، `'error'`) فراهم میکند.
import * as fs from 'fs';
const largeFilePath: string = 'data/large_file.txt';
const copiedFilePath: string = 'data/copied_file.txt';
// ایجاد یک فایل بزرگ ساختگی برای نمایش
function createLargeFile(path: string, sizeInMB: number): void {
const content: string = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '; // 56 کاراکتر
const stream = fs.createWriteStream(path);
const totalChars = sizeInMB * 1024 * 1024; // تبدیل مگابایت به بایت
const iterations = Math.ceil(totalChars / content.length);
for (let i = 0; i < iterations; i++) {
stream.write(content);
}
stream.end(() => console.log(`Created large file '${path}' (${sizeInMB}MB).`));
}
// برای نمایش، ابتدا اطمینان حاصل میکنیم که دایرکتوری 'data' وجود دارد
fs.mkdir('data', { recursive: true }, (err) => {
if (err && err.code !== 'EEXIST') {
console.error('Error creating data directory:', err);
return;
}
createLargeFile(largeFilePath, 1); // ایجاد یک فایل 1 مگابایتی
});
// کپی کردن فایل با استفاده از استریمها
function copyFileWithStreams(source: string, destination: string): void {
const readStream = fs.createReadStream(source);
const writeStream = fs.createWriteStream(destination);
readStream.on('open', () => console.log(`Reading stream for '${source}' opened.`));
writeStream.on('open', () => console.log(`Writing stream for '${destination}' opened.`));
// انتقال داده از استریم خواندنی به استریم نوشتنی
readStream.pipe(writeStream);
readStream.on('error', (err: Error) => {
console.error(`Read stream error: ${err.message}`);
});
writeStream.on('error', (err: Error) => {
console.error(`Write stream error: ${err.message}`);
});
writeStream.on('finish', () => {
console.log(`File '${source}' copied to '${destination}' successfully using streams.`);
// پاکسازی فایل بزرگ ساختگی پس از کپی
fs.unlink(largeFilePath, (err) => {
if (err) console.error('Error deleting large file:', err);
else console.log(`Large file '${largeFilePath}' deleted.`);
});
});
}
// کمی صبر میکنیم تا فایل بزرگ ایجاد شود قبل از تلاش برای کپی کردن
setTimeout(() => {
copyFileWithStreams(largeFilePath, copiedFilePath);
}, 1000);
نظارت بر تغییرات: `fs.watch`, `fs.watchFile`
نظارت بر سیستم فایل برای تغییرات برای وظایفی مانند بارگذاری مجدد سرورهای توسعه (hot-reloading)، فرآیندهای ساخت (build) یا همگامسازی بیدرنگ دادهها حیاتی است. Node.js دو روش اصلی برای این کار ارائه میدهد: `fs.watch` و `fs.watchFile`. تایپاسکریپت تضمین میکند که انواع رویدادها و پارامترهای شنونده (listener) به درستی مدیریت شوند.
`fs.watch`: نظارت مبتنی بر رویداد سیستم فایل
`fs.watch` به طور کلی کارآمدتر است زیرا اغلب از اعلانهای سطح سیستم عامل (مانند `inotify` در لینوکس، `kqueue` در macOS، `ReadDirectoryChangesW` در ویندوز) استفاده میکند. این برای نظارت بر فایلها یا دایرکتوریهای خاص برای تغییرات، حذفها یا تغییر نامها مناسب است.
import * as fs from 'fs';
const watchedFilePath: string = 'data/watched_file.txt';
const watchedDirPath: string = 'data/watched_dir';
// اطمینان از وجود فایلها/دایرکتوریها برای نظارت
fs.writeFileSync(watchedFilePath, 'Initial content.');
fs.mkdirSync(watchedDirPath, { recursive: true });
console.log(`Watching '${watchedFilePath}' for changes...`);
const fileWatcher = fs.watch(watchedFilePath, (eventType: string, filename: string | Buffer | null) => {
const fname = typeof filename === 'string' ? filename : filename?.toString('utf8');
console.log(`File '${fname || 'N/A'}' event: ${eventType}`);
if (eventType === 'change') {
console.log('File content potentially changed.');
}
// در یک برنامه واقعی، ممکن است در اینجا فایل را بخوانید یا یک فرآیند ساخت را راهاندازی کنید
});
console.log(`Watching directory '${watchedDirPath}' for changes...`);
const dirWatcher = fs.watch(watchedDirPath, (eventType: string, filename: string | Buffer | null) => {
const fname = typeof filename === 'string' ? filename : filename?.toString('utf8');
console.log(`Directory '${watchedDirPath}' event: ${eventType} on '${fname || 'N/A'}'`);
});
fileWatcher.on('error', (err: Error) => console.error(`File watcher error: ${err.message}`));
dirWatcher.on('error', (err: Error) => console.error(`Directory watcher error: ${err.message}`));
// شبیهسازی تغییرات پس از یک تأخیر
setTimeout(() => {
console.log('\n--- Simulating changes ---');
fs.appendFileSync(watchedFilePath, '\nNew line added.');
fs.writeFileSync(`${watchedDirPath}/new_file.txt`, 'Content.');
fs.unlinkSync(`${watchedDirPath}/new_file.txt`); // همچنین تست حذف
setTimeout(() => {
fileWatcher.close();
dirWatcher.close();
console.log('\nWatchers closed.');
// پاکسازی فایلها/دایرکتوریهای موقت
fs.unlinkSync(watchedFilePath);
fs.rmSync(watchedDirPath, { recursive: true, force: true });
}, 2000);
}, 1000);
نکته در مورد `fs.watch`: این همیشه در تمام پلتفرمها برای همه انواع رویدادها قابل اعتماد نیست (مثلاً تغییر نام فایل ممکن است به عنوان حذف و ایجاد گزارش شود). برای نظارت قوی چندپلتفرمی بر فایل، کتابخانههایی مانند `chokidar` را در نظر بگیرید که اغلب از `fs.watch` در زیرساخت خود استفاده میکنند اما مکانیزمهای نرمالسازی و جایگزین اضافه میکنند.
`fs.watchFile`: نظارت مبتنی بر نظرسنجی (Polling) بر فایل
`fs.watchFile` از نظرسنجی (بررسی دورهای دادههای `stat` فایل) برای تشخیص تغییرات استفاده میکند. این کارایی کمتری دارد اما در سیستمهای فایل مختلف و درایوهای شبکه سازگارتر است. این برای محیطهایی که `fs.watch` ممکن است غیرقابل اعتماد باشد (مثلاً اشتراکگذاریهای NFS) مناسبتر است.
import * as fs from 'fs';
import { Stats } from 'fs';
const pollFilePath: string = 'data/polled_file.txt';
fs.writeFileSync(pollFilePath, 'Initial polled content.');
console.log(`Polling '${pollFilePath}' for changes...`);
fs.watchFile(pollFilePath, { interval: 1000 }, (curr: Stats, prev: Stats) => {
// تایپاسکریپت تضمین میکند که 'curr' و 'prev' اشیاء fs.Stats هستند
if (curr.mtimeMs !== prev.mtimeMs) {
console.log(`File '${pollFilePath}' modified (mtime changed). New size: ${curr.size} bytes.`);
}
});
setTimeout(() => {
console.log('\n--- Simulating polled file change ---');
fs.appendFileSync(pollFilePath, '\nAnother line added to polled file.');
setTimeout(() => {
fs.unwatchFile(pollFilePath);
console.log(`\nStopped watching '${pollFilePath}'.`);
fs.unlinkSync(pollFilePath);
}, 2000);
}, 1500);
مدیریت خطا و بهترین شیوهها در یک زمینه جهانی
مدیریت خطای قوی برای هر برنامه آماده تولید، به ویژه برنامهای که با سیستم فایل تعامل دارد، بسیار مهم است. عملیات فایل میتواند به دلایل متعددی با شکست مواجه شود: مشکلات مجوز، خطاهای پر بودن دیسک، یافت نشدن فایل، خطاهای ورودی/خروجی، مشکلات شبکه (برای درایوهای متصل به شبکه) یا تداخل دسترسی همزمان. تایپاسکریپت به شما در گرفتن مسائل مربوط به نوع کمک میکند، اما خطاهای زمان اجرا هنوز به مدیریت دقیق نیاز دارند.
استراتژیهای مدیریت خطا
- عملیات همگام: همیشه فراخوانیهای `fs.xxxSync` را در بلوکهای `try...catch` قرار دهید. این متدها خطاها را مستقیماً پرتاب میکنند.
- Callbackهای ناهمگام: اولین آرگومان یک callback `fs` همیشه `err: NodeJS.ErrnoException | null` است. همیشه ابتدا این شی `err` را بررسی کنید.
- مبتنی بر Promise (`fs/promises`): از `try...catch` با `await` یا `.catch()` با زنجیرههای `.then()` برای مدیریت رد شدنها (rejections) استفاده کنید.
استانداردسازی فرمتهای ثبت خطا و در نظر گرفتن بینالمللیسازی (i18n) برای پیامهای خطا اگر بازخورد خطای برنامه شما رو به کاربر باشد، مفید است.
import * as fs from 'fs';
import { promises as fsPromises } from 'fs';
import * as path from 'path';
const problematicPath = path.join('non_existent_dir', 'file.txt');
// مدیریت خطای همگام
try {
fs.readFileSync(problematicPath, 'utf8');
} catch (error: any) {
console.error(`Sync Error: ${error.code} - ${error.message} (Path: ${problematicPath})`);
}
// مدیریت خطای مبتنی بر Callback
fs.readFile(problematicPath, 'utf8', (err, data) => {
if (err) {
console.error(`Callback Error: ${err.code} - ${err.message} (Path: ${problematicPath})`);
return;
}
// ... پردازش داده
});
// مدیریت خطای مبتنی بر Promise
async function safeReadFile(filePath: string): Promise
مدیریت منابع: بستن توصیفگرهای فایل
هنگام کار با `fs.open` (یا `fsPromises.open`)، بسیار مهم است که اطمینان حاصل شود که توصیفگرهای فایل همیشه پس از اتمام عملیات با استفاده از `fs.close` (یا `fileHandle.close()`) بسته میشوند، حتی اگر خطا رخ دهد. عدم انجام این کار میتواند منجر به نشت منابع، رسیدن به حد فایلهای باز سیستم عامل و به طور بالقوه از کار افتادن برنامه شما یا تأثیر بر سایر فرآیندها شود.
API `fs/promises` با اشیاء `FileHandle` به طور کلی این کار را ساده میکند، زیرا `fileHandle.close()` به طور خاص برای این منظور طراحی شده است و نمونههای `FileHandle` قابل دفع (Disposable) هستند (اگر از Node.js 18.11.0+ و TypeScript 5.2+ استفاده میکنید).
مدیریت مسیر و سازگاری چندپلتفرمی
مسیرهای فایل بین سیستم عاملها به طور قابل توجهی متفاوت است (مثلاً `\` در ویندوز، `/` در سیستمهای شبه یونیکس). ماژول `path` در Node.js برای ساخت و تجزیه مسیرهای فایل به روشی سازگار با چند پلتفرم، که برای استقرارهای جهانی ضروری است، indispensable است.
- `path.join(...paths)`: تمام بخشهای مسیر داده شده را به هم متصل کرده و مسیر حاصل را نرمال میکند.
- `path.resolve(...paths)`: دنبالهای از مسیرها یا بخشهای مسیر را به یک مسیر مطلق حل میکند.
- `path.basename(path)`: آخرین بخش یک مسیر را برمیگرداند.
- `path.dirname(path)`: نام دایرکتوری یک مسیر را برمیگرداند.
- `path.extname(path)`: پسوند مسیر را برمیگرداند.
تایپاسکریپت تعاریف نوع کامل برای ماژول `path` را فراهم میکند و تضمین میکند که از توابع آن به درستی استفاده کنید.
import * as path from 'path';
const dir = 'my_app_data';
const filename = 'config.json';
// اتصال مسیر چندپلتفرمی
const fullPath: string = path.join(__dirname, dir, filename);
console.log(`Cross-platform path: ${fullPath}`);
// دریافت نام دایرکتوری
const dirname: string = path.dirname(fullPath);
console.log(`Directory name: ${dirname}`);
// دریافت نام پایه فایل
const basename: string = path.basename(fullPath);
console.log(`Base name: ${basename}`);
// دریافت پسوند فایل
const extname: string = path.extname(fullPath);
console.log(`Extension: ${extname}`);
همزمانی و شرایط رقابتی (Race Conditions)
هنگامی که چندین عملیات فایل ناهمگام به طور همزمان آغاز میشوند، به ویژه نوشتن یا حذف، شرایط رقابتی میتواند رخ دهد. به عنوان مثال، اگر یک عملیات وجود یک فایل را بررسی کند و دیگری قبل از اقدام عملیات اول آن را حذف کند، ممکن است عملیات اول به طور غیرمنتظرهای با شکست مواجه شود.
- از `fs.existsSync` برای منطق مسیر بحرانی خودداری کنید؛ `fs.access` را ترجیح دهید یا به سادگی عملیات را امتحان کرده و خطا را مدیریت کنید.
- برای عملیاتی که نیاز به دسترسی انحصاری دارند، از گزینههای `flag` مناسب استفاده کنید (مثلاً `'wx'` برای نوشتن انحصاری).
- برای دسترسی به منابع مشترک بسیار حیاتی، مکانیزمهای قفلگذاری (مانند قفلهای فایل یا قفلهای سطح برنامه) را پیادهسازی کنید، هرچند این کار پیچیدگی را افزایش میدهد.
مجوزها (ACLs)
مجوزهای سیستم فایل (لیستهای کنترل دسترسی یا مجوزهای استاندارد یونیکس) یک منبع رایج خطا هستند. اطمینان حاصل کنید که فرآیند Node.js شما مجوزهای لازم برای خواندن، نوشتن یا اجرای فایلها و دایرکتوریها را دارد. این به ویژه در محیطهای کانتینری یا در سیستمهای چند کاربره که فرآیندها با حسابهای کاربری خاص اجرا میشوند، مرتبط است.
نتیجهگیری: پذیرش ایمنی نوع برای عملیات جهانی سیستم فایل
ماژول `fs` در Node.js یک ابزار قدرتمند و همهکاره برای تعامل با سیستم فایل است که طیفی از گزینهها از دستکاریهای ساده فایل تا پردازش دادههای پیشرفته مبتنی بر استریم را ارائه میدهد. با لایهبندی تایپاسکریپت بر روی این عملیات، شما از مزایای ارزشمندی بهرهمند میشوید: تشخیص خطای زمان کامپایل، وضوح کد بهبود یافته، پشتیبانی ابزاری برتر و افزایش اطمینان در حین بازسازی کد. این به ویژه برای تیمهای توسعه جهانی که در آن سازگاری و کاهش ابهام در پایگاههای کد متنوع حیاتی است، بسیار مهم است.
چه در حال ساخت یک اسکریپت کوچک ابزاری باشید یا یک برنامه سازمانی در مقیاس بزرگ، استفاده از سیستم نوع قوی تایپاسکریپت برای عملیات فایل Node.js شما منجر به کدی قابل نگهداریتر، قابل اعتمادتر و مقاومتر در برابر خطا خواهد شد. API `fs/promises` را برای الگوهای ناهمگام تمیزتر بپذیرید، تفاوتهای ظریف بین فراخوانیهای همگام و ناهمگام را درک کنید و همیشه مدیریت خطای قوی و مدیریت مسیر چندپلتفرمی را در اولویت قرار دهید.
با به کارگیری اصول و مثالهای مورد بحث در این راهنما، توسعهدهندگان در سراسر جهان میتوانند تعاملاتی با سیستم فایل بسازند که نه تنها کارآمد و بهینه هستند، بلکه ذاتاً امنتر و آسانتر برای استدلال هستند و در نهایت به تحویل نرمافزار با کیفیت بالاتر کمک میکنند.